﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using PropertyExpression.Common;

namespace PropertyExpression.ControllableQuery
{
    public struct Param<T>
    {
        internal readonly ParamContent<T> Content;

        internal Param(ParamContent<T> content)
        {
            Content = content;
        }

        public static implicit operator QItem(Param<T> value)
        {
            return new QItem(builder => builder.Append(value));
        }
    }

    internal class ParamContent<T>
    {
        internal readonly Func<string, SqlParameter> SqlParameterFunc;
        internal readonly Func<T, object> ToSqlParameterValueFunc;
        internal readonly Option<Func<T>> ValueFunc;

        public ParamContent(Func<string, SqlParameter> sqlParameterFunc, Func<T, object> toSqlParameterValueFunc, Option<Func<T>> valueFunc)
        {
            SqlParameterFunc = sqlParameterFunc;
            ToSqlParameterValueFunc = toSqlParameterValueFunc;
            ValueFunc = valueFunc;
        }
    }

    public static class Param
    {
        public static Param<Guid> New(Guid value)
        {
            return New(() => value);
        }

        public static Param<Guid> New(Func<Guid> value)
        {
            return New(GuidInfo, value);
        }

        public static Param<Guid> NewGuid()
        {
            return New(GuidInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<Guid, object>> GuidInfo
        {
            get { return NewInfo<Guid>(name => new SqlParameter(name, SqlDbType.UniqueIdentifier), _ => _); }
        }

        public static Param<Option<Guid>> New(Option<Guid> value)
        {
            return New(() => value);
        }

        public static Param<Option<Guid>> New(Func<Option<Guid>> value)
        {
            return New(OptionalGuidInfo, value);
        }

        public static Param<Option<Guid>> NewOptionalGuid()
        {
            return New(OptionalGuidInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<Option<Guid>, object>> OptionalGuidInfo
        {
            get { return NewInfo<Option<Guid>>(name => new SqlParameter(name, SqlDbType.UniqueIdentifier), GetValueOrDbNull); }
        }

        public static Param<bool> New(bool value)
        {
            return New(() => value);
        }

        public static Param<bool> New(Func<bool> value)
        {
            return New(BoolInfo, value);
        }

        public static Param<bool> NewBool()
        {
            return New(BoolInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<bool, object>> BoolInfo
        {
            get { return NewInfo<bool>(GetBooleanSqlParameterFunc(), _ => _); }
        }

        public static Param<Option<bool>> New(Option<bool> value)
        {
            return New(() => value);
        }

        public static Param<Option<bool>> New(Func<Option<bool>> value)
        {
            return New(OptionalBool, value);
        }

        public static Param<Option<bool>> NewOptionalBool()
        {
            return New(OptionalBool);
        }

        private static Tuple<Func<string, SqlParameter>, Func<Option<bool>, object>> OptionalBool
        {
            get { return NewInfo<Option<bool>>(GetBooleanSqlParameterFunc(), GetValueOrDbNull); }
        }

        public static Param<float> New(float value)
        {
            return New(() => value);
        }

        public static Param<float> New(Func<float> value)
        {
            return New(FloatInfo, value);
        }

        public static Param<float> NewFloat()
        {
            return New(FloatInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<float, object>> FloatInfo
        {
            get { return NewInfo<float>(GetFloatSqlParameterFunc(), _ => _); }
        }

        public static Param<Option<float>> New(Option<float> value)
        {
            return New(() => value);
        }

        public static Param<Option<float>> New(Func<Option<float>> value)
        {
            return New(OptionalFloatInfo, value);
        }

        public static Param<Option<float>> NewOptionalFloat()
        {
            return New(OptionalFloatInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<Option<float>, object>> OptionalFloatInfo
        {
            get { return NewInfo<Option<float>>(GetFloatSqlParameterFunc(), GetValueOrDbNull); }
        }

        public static Param<string> New(string value)
        {
            return New(() => value);
        }

        public static Param<string> New(Func<string> value)
        {
            return New(StringInfo, value);
        }

        public static Param<string> NewString()
        {
            return New(StringInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<string, object>> StringInfo
        {
            get { return NewInfo<string>(GetStringSqlParameterFunc(), _ => _); }
        }

        public static Param<DateTime> New(DateTime value)
        {
            return New(() => value);
        }

        public static Param<DateTime> New(Func<DateTime> value)
        {
            return New(DateTimeInfo, value);
        }

        public static Param<DateTime> NewDateTime()
        {
            return New(DateTimeInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<DateTime, object>> DateTimeInfo
        {
            get { return NewInfo<DateTime>(name => new SqlParameter(name, SqlDbType.DateTime), _ => _); }
        }

        public static Param<Option<string>> New(Option<string> value)
        {
            return New(() => value);
        }

        public static Param<Option<string>> New(Func<Option<string>> value)
        {
            return New(OptionalStringInfo, value);
        }

        public static Param<Option<string>> NewOptionalString()
        {
            return New(OptionalStringInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<Option<string>, object>> OptionalStringInfo
        {
            get { return NewInfo<Option<string>>(GetStringSqlParameterFunc(), GetValueOrDbNull); }
        }

        public static Param<int> New(int value)
        {
            return New(() => value);
        }

        public static Param<int> New(Func<int> value)
        {
            return New(IntInfo, value);
        }

        public static Param<int> NewInt()
        {
            return New(IntInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<int, object>> IntInfo
        {
            get { return NewInfo<int>(name => new SqlParameter(name, SqlDbType.Int), _ => _); }
        }

        public static Param<Option<int>> New(Option<int> value)
        {
            return New(() => value);
        }

        public static Param<Option<int>> New(Func<Option<int>> value)
        {
            return New(OptionalIntInfo, value);
        }

        public static Param<Option<int>> NewOptionalInt()
        {
            return New(OptionalIntInfo);
        }

        private static Tuple<Func<string, SqlParameter>, Func<Option<int>, object>> OptionalIntInfo
        {
            get { return NewInfo<Option<int>>(name => new SqlParameter(name, SqlDbType.Int), GetValueOrDbNull); }
        }

        public static Param<ITableValued<T>> New<T>(ITableValued<T> value)
        {
            return New(() => value);
        }

        public static Param<ITableValued<T>> New<T>(Func<ITableValued<T>> value)
        {
            return New(TableValuedInfo<T>(), value);
        }

        public static Param<ITableValued<T>> NewTableValued<T>()
        {
            return New(TableValuedInfo<T>());
        }

        private static Tuple<Func<string, SqlParameter>, Func<ITableValued<T>, object>> TableValuedInfo<T>()
        {
            return NewInfo<ITableValued<T>>(
                parameterName => new SqlParameter(parameterName, SqlDbType.Structured) {
                    TypeName = typeof (T).Name
                },
                _ => ConvertToDataTable(_.Value));
        }

        private static Func<string, SqlParameter> GetFloatSqlParameterFunc()
        {
            return name => new SqlParameter(name, SqlDbType.Float);
        }

        private static Func<string, SqlParameter> GetStringSqlParameterFunc()
        {
            return name => new SqlParameter(name, SqlDbType.NVarChar) { Size = -1 };
        }

        private static Func<string, SqlParameter> GetBooleanSqlParameterFunc()
        {
            return name => new SqlParameter(name, SqlDbType.Bit);
        }

        private static object GetValueOrDbNull<T>(this Option<T> value)
        {
            return value.Match<object>(_ => _, () => DBNull.Value);
        }

        private static DataTable ConvertToDataTable<T>(IEnumerable<T> value)
        {
            var dataTable = new DataTable();
            var propertyInfos = typeof(T).GetProperties();
            foreach (var propertyInfo in propertyInfos)
            {
                dataTable.Columns.Add(propertyInfo.Name, propertyInfo.PropertyType);
            }
            foreach (var item in value)
            {
                foreach (var propertyInfo in propertyInfos)
                {
                    var dataRow = dataTable.NewRow();
                    dataRow[propertyInfo.Name] = propertyInfo.GetValue(item, null);
                    dataTable.Rows.Add(dataRow);
                }
            }
            return dataTable;
        }

        private static Tuple<Func<string, SqlParameter>, Func<T, object>> NewInfo<T>(
            Func<string, SqlParameter> sqlParameterFunc,
            Func<T, object> toSqlParameterValueFunc)
        {
            return Tuple.Create(sqlParameterFunc, toSqlParameterValueFunc);
        }

        private static Param<T> New<T>(Tuple<Func<string, SqlParameter>, Func<T, object>> tuple)
        {
            return new Param<T>(new ParamContent<T>(tuple.Item1, tuple.Item2, new Option<Func<T>>()));
        }

        private static Param<T> New<T>(Tuple<Func<string, SqlParameter>, Func<T, object>> tuple, Func<T> valueFunc)
        {
            return new Param<T>(new ParamContent<T>(tuple.Item1, tuple.Item2, valueFunc));
        }
    }

    public static class ParamExtensions
    {
        public static Param<Guid> Param(this Guid value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<Guid>> Param(this Option<Guid> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<bool> Param(this bool value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<bool>> Param(this Option<bool> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<float> Param(this float value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<float>> Param(this Option<float> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<string> Param(this string value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<DateTime> Param(this DateTime value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<string>> Param(this Option<string> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<int> Param(this int value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<int>> Param(this Option<int> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<ITableValued<T>> Param<T>(this ITableValued<T> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<ITableValued<T>> Param<T>(this IEnumerable<T> value)
        {
            return ControllableQuery.Param.New(value.AsTableValued());
        }
    }
}